<?php if (!defined('PmWiki')) exit();
/*
   Author: Martin Fick
   Requires: markupsamepass.php
   Date: 2008-11-26
*/

define(CONTENT_VERSION, '1.8');

# ---------- Configuration Section --------------

# $ContentCfgPreviewKey = 'To enable preview signing, set this key!';

#    To enable viewing logs from the web with action=contentlog
#    set this to the authorization level required to acces the
#    logs (read|edit)'
# $ContentCfgLogViewer = 'edit';


SDV($ContentCfgPropPrefix, 'content_');  # Prefix for page properties
SDV($ContentCfgCacheDir, "/var/tmp/content"); # cache and tmp file location.  This is needed even if the cache is disabled.

# You may completely disable the cache system here.
$ContentCfgCacheEnabled = true;

# ------------------------------------------------

if(! $MARKUP_SAME_PASS_VERSION) include_once('markupsamepass.php');
if(! $CONTENT_DIAGS_VERSION) @ include_once('contentdiags.php');

SDVA($HandleActions, array('content' => 'ContentAction'));
if($ContentCfgLogViewer)  SDVA($HandleActions, array('contentlog' => 'ContentActionLog'));

$EditFunctions[] = 'ContentAfterSave';
array_unshift($EditFunctions, 'ContentClearProperties');

# Default embeddable types which get inserted directly in the output text
ContentRegisterType('inline', null, 'txt', null, null);
ContentRegisterType('safe', null, 'txt', null, null);
ContentRegisterType('raw');

$ContentCacheEnable = ContentCfgCacheEnabled;

$ContentPageNamePat = "!?[-\\w.\\/\\x80-\\xff]*";  // a valid PageName

Markup('(:content:)', 'fulltext',
    '/\\(:content\s+(.*?)\s*:\\)\n?(.*?)\\(:contentend:\\)/es',
    "PRR(ContentDirective(\$pagename, ContentHandleFullText(PSS('\$2')), ParseArgs(PSS('\$1'))))");

Markup('(:contentlist:)', '<(:contentcurrent:)',
    '/\\(:contentlist\s+(.*?)\s*:\\)/es',
    "PRR(ContentDirective(\$pagename, null, ParseArgs(PSS('\$1'))))");

# Internal directive, do not use in wikipages!
MarkupSamePass('(:contentcurrent:)', '>(:content:)', array(
      '\\(:contentset\s+(\\w+)\s+(\\w+)\s*:\\)' =>
        "PRR(ContentSetPointer('\$1', '\$2'))",

      '\\{\\$\\/\\.\\.([\\w:,.\\/]+)(~\\w+)(\\+)?\\}' =>
        "PRR('{\$/'.ContentCurrent('type').'..$1$2$3}')",
      '\\{\\$\\/([\\w:,.\\/]+)(\\+)?\\}' =>
        "PRR('{\$/\$1~'.ContentCurrent('name').'\$2}')"),
  'e');

Markup('{PageName$/cpath~name}', '>(:contentcurrent:)',
       "/\\{($ContentPageNamePat)?".'\\$(\\/[\\w:,.\\/]+~\\w+)(\\+)?\\}/e',
       "PRR(ContentPV(\$pagename, PSS('\$0'), '+' == '\$3'))");

# Internal directive, do not use in wikipages!
Markup('(:contentimages:)', '>{PageName$/cpath~name}',
    '/\\(:contentimages\s+(.*?)\s*:\\)/e',
    "ContentMultiImages(PSS('\$1'))");

Markup('[[*|name]]', '<links',
       '/\\[\\[ ?( *[^]| ]+?)(( +[^]| ]+?){1,}) *\\| *(.*?) *\\]\\]/e',
       "ContentEnumAlink(PSS('\$1'), PSS('\$2'), PSS('\$4'))");

// Internal Directive: Take a list of image URLs and
//  place them one after another
function ContentMultiImages($list) {
  $list = preg_replace('/\s+/', ' ', $list);
  $list = explode(' ', $list);
  return implode(' '.keep('<br/><br/><br/>').' ', $list);
}

// Markup: Take a list of URLs and enumerated using the same text
function ContentEnumAlink($url1, $urls, $text) {
  $eurls = explode(' ', $url1.$urls);
  foreach($eurls as $k => $url) {
    if($url) $links[]= "[[$url|".($k+1)."]]";
  }
  return "$text(".implode(' ', $links).")";
}

// Internal Directive: Set the current content pointer (name, type)
function ContentSetPointer($type, $name) {
  global $ContentCurrent;

  $ContentCurrent['type']= $type;
  $ContentCurrent['names'][$type]= $name;
}

// Internal Directive: Get the current content pointer (name, type)
function ContentCurrent($def, $type=null) {
  global $ContentCurrent;

  if($type === null) $type = $ContentCurrent['type'];
  if($type !== null) {
    $name = $ContentCurrent['names'][$type];
    if($def === 'type')  return $type;
    if($def === 'name')  return $name;
    if($def === 'type name')  return "$type $name";
  }
}

// Directive: Register some content from a directive and/or
//  get a list of output types from a directive
function ContentDirective($cpn, $csrc, $args) {
  global $ContentTypes, $ContentTypeNames;

  $type = $args[''][0]; if ($type === null) return;
  $name = $args[''][1];
  if($ContentTypes[$type]['directive'] === false) return;

  $pn = $cpn;
  if($csrc !== null)
    $set = ContentSave($cpn, $csrc, $type, $name, $args['defargs'])."\n";
  elseif(isset($args['page'])) $pn = $args['page'];
  if($name === null)  $name = $ContentTypeNames[$type];
  $dargs = ContentGetProp($cpn, $pn, $type, $name, 'args');
  $types = ContentTypesText($cpn, $pn, $type, $name, $args, $dargs);
  return $set.$types;
}

// Markup: Creates a link for a cpath, even embedded or inline
// types (so that users can expilicitly get links to the text
// of the inline types).  Unless the $nocheck is set, URLs that
// create blank outputs are blanked.
function ContentPV($cpn, $cpath, $nocheck) {
  $cp = ContentParsePath($cpath, $cpn);
  if(!$nocheck) {
    $cp = ContentCPFillData($cpn, $cp);
    $out = ContentGetCP($cp);
    if(is_array($out)) {
      foreach($out as $cpath)  $urls[] = ContentPV($cpn, $cpath, $nocheck);
      return implode(' ', $urls);
    }
    if (!$out)  return;
  }
  return ContentURL($cp, $cpn);
}

// API:  Register a Type
function ContentRegisterType($type, $mime=null, $ext=null, $deflist=null,
  $directive=false, $handler=null) {
  global $ContentTypes;
  if(!$type)  return false;

  $ts['type'] = $type;
  $ts['mime'] = $mime;
  if($deflist !== null)  $ts['list'] = $deflist;
  if($ext === '')        $ts['ext']  = $type;
  else                   $ts['ext']  = $ext;
  $ts['handler']  = $handler;
  $ts['directive']  = $directive;

  $ContentTypes[$type] = $ts;

  if($directive) {
    if($directive === true) $directive = $type;
    Markup("(:$directive:)", '<(:content:)',
      "/\\(:$directive(\s+.*?)?:\\)(.*?)\\(:${directive}end:\\)/s",
      "(:content $type\$1:)\$2(:contentend:)");
  }
}

// API: Register a Converter
function ContentRegConverter($intype, $outtype, $fnc, $cnv=null) {
  global $ContentConverters, $ContentIndex;

  if ($cnv === null)  $cnv = $intype .'2'. $outtype;
  $ContentConverters[$cnv]['intype']  = $intype;
  $ContentConverters[$cnv]['outtype'] = $outtype;
  $ContentConverters[$cnv]['fnc'] = $fnc;

// [in_out][intype][outtype] -> converter   (~eventually, multi)
// [out_in][outtype][intype] -> converter   (~eventually, multi)
// Multi paths simply default to the last one I think, will be enhanced.
//  by creating converter arrays and merging arrays.
// I have not considered loops yet, will they crash, or worse, infinite loop?

  // Converters cnv_i can create our intype from itype
  foreach((array)$ContentIndex['out_in'][$intype] as $itype => $cnv_i) {
    // Make sure that they now can create our outtype from their itype
    $ContentIndex['in_out'][$itype][$outtype] = $cnv_i;
    $ContentIndex['out_in'][$outtype][$itype] = $cnv_i;

    // Converters cnv_o can create otype from our outtype
    foreach((array)$ContentIndex['in_out'][$outtype] as $otype => $cnv_o) {
      // Make sure that they can now create otype from all
      // the itypes of cnv_i
      $ContentIndex['in_out'][$itype][$otype] = $cnv_i;
      $ContentIndex['out_in'][$otype][$itype] = $cnv_i;
    }
  }

  // Add the new converters stuff to the indexes
  $ContentIndex['in_out'][$intype][$outtype] = $cnv;
  $ContentIndex['out_in'][$outtype][$intype] = $cnv;

  // Converters cnv_o can create otype from our outtype
  foreach((array)$ContentIndex['in_out'][$outtype] as $otype => $cnv_o) {
    // Make sure that we can generate their otype from our intype
    $ContentIndex['in_out'][$intype][$otype] = $cnv;
    $ContentIndex['out_in'][$otype][$intype] = $cnv;
  }
}

// API: Register an FS Converter
function ContentRegFSConverter($intype, $outtype, $cmdfmt, $cnv=null,
                               $fnc=null) {
  global $ContentConverters;

  if ($cnv === null)  $cnv = $intype .'2'. $outtype;
  $ContentConverters[$cnv]['cmdfmt']  = $cmdfmt;;
  $ContentConverters[$cnv]['argfnc']  = $fnc;
  ContentRegConverter($intype, $outtype, 'ContentFSConverter', $cnv);
}

// Lookup and link to all the output types of a type.
function ContentTypesText($cpn, $pn, $type, $name, $args=null, $dargs=null) {
  global $ImgExtPattern;

  $cpaths = ContentPathsFrom($type, $name);
  $cargs  = ContentParseCnvArgs($args['args'], $type);
  $dcargs = ContentParseCnvArgs($dargs, $type);
  $cpaths = ContentApplyCArgs($cpaths, $cargs);
  $cpaths = ContentApplyCArgs($cpaths, $dcargs);
  $types = ContentFilterTypes($cpaths, $args);

  $embed= $args['embed'] != strtolower("false");
  if(in_array('nocheck', $args[''], true)) $nocheck = "+";
  foreach($types as $type => $cpath) {
    $cpath = "{"."$pn\$$cpath$nocheck}";
    if($type == 'inline' || $type == 'safe' || $type == 'raw') {
      $cp = ContentCPFillData($cpn, ContentParsePath($cpath));
      $inline = ContentGetCP($cp);
      $esc = array('escape' => 0);
      if($type != 'raw')    $inline = htmlentities($inline);
      if($type == 'safe')   $inline = MarkupToHTML($pn, $inline, $esc);
      if($type != 'inline') $inline = Keep($inline);
    }
    elseif($embed && preg_match("/$ImgExtPattern/", '.'.$type, $m))
         $ilinks[] = "$cpath";
    else $tlinks[] = "[[$cpath | $type]]";
  }
  if($ilinks) $out = '(:contentimages '.implode(' ', $ilinks).':)';
  if($inline) {
    if($ilinks) $out .= ' <br/><br/> ';
    $out .= $inline;
  }
  if($tlinks) {
    if($ilinks) $out .= ' <br/><br/> ';
    $out .= implode(' ', $tlinks);
  }
  return $out;
}

// Apply a ',' comma separated list to another list
function ContentApplyOptList($deflist, $optlist) {
  $opts = explode(',', $optlist);
  foreach($deflist as $opt) $list[$opt] = $opt;

  foreach($opts as $opt)
      if ($opt{0} != '+' && $opt{0} != '-') unset($list);

  foreach($opts as $opt) {
    if    ($opt{0} === '+')  $list[substr($opt, 1)] = $opt;
    elseif($opt{0} === '-')  unset($list[substr($opt, 1)]);
    else $list[$opt] = $opt;
  }
  return $list;
}

// Filter the list of types
function ContentFilterTypes($types, $args){
  global $ContentTypes;
  $type = $args[''][0];
  $list = $args['list'];

  $olist = (array) $types;
  if(!isset($args['list']) || $list === 'default') $list = 'defaults';
  if($list === 'defaults') {
    if(isset($ContentTypes[$type]['list']))
      $olist = ContentApplyOptList(array_keys($olist),
         $ContentTypes[$type]['list']);
    else $list = 'all';
  }
  if($list === 'none')  unset($olist);

  $olist = (array)$olist;
  if(isset($args['types']))
    $olist = ContentApplyOptList(array_keys($olist), $args['types']);

  foreach($types as $type => $cpath)
    if(isset($olist[$type])) $out[$type] = $cpath;

  return (array)$out;
}

// Return the cpath(s) from one type to another type
function ContentPath2($inseg, $outseg, $cp=null) {
  global $ContentConverters, $ContentIndex;

  $itseg = ContentParseTseg($inseg);
  $intype =  $itseg['type'];
  $otseg = ContentParseTseg($outseg, $intype);
  $outtype = $otseg['type'];

  if($intype == $outtype) {
    if(!$otseg['cnv'])  $otseg['cnv']  = $itseg['cnv'];
    if(!$otseg['args']) $otseg['args'] = $itseg['args'];
    return ContentMkSeg($otseg);
  }
  if($cp['type']  == null) $cp['type'] = $intype;
  if($cp['tsegs'] == null) $cp['tsegs'][] = $itseg;

  $cnv = $otseg['cnv'];
  if($ContentConverters[$cnv]['outtype'] !== $outtype)  $cnv=null;
  if(!$cnv) $cnv = $ContentIndex['in_out'][$intype][$outtype];
  if(!$cnv) return '/';
  $otype = $ContentConverters[$cnv]['outtype'];
  if ($otype == $outtype) $args = $otseg['args'];

  $cp['tsegs'][] = array('type'=>$otype, 'cnv'=>$cnv, 'args'=>$args);
  if ($otype != $outtype) return ContentPath2($otype, $outseg, $cp);
  return ContentMkPath($cp);
}

// Retrieve a content's source/properties from the PCACHE
function ContentGetProp($cpn, $pn, $type, $name, $cprop=null) {
  global $FmtPV, $ContentCfgPropPrefix, $ContentTypes, $PCache;
  if($ContentTypes[$type]['directive'] === false) return; // forbidden
  $prop = "$ContentCfgPropPrefix${type}_$name";
  if($cprop) $prop .= ".$cprop";
  $FmtPV[$prop]="\$page['$prop']";
  // For some reason pagevar != property for current page
  // Pmwiki should check auth for current page
  if(MakePageName($cpn, $pn) === $cpn) return $PCache[$pn]["=p_$prop"];
  if(!CondAuth($pn, 'read')) return;  // unauthorized
  $val = PageVar($cpn, $prop, $pn);
  return $val;
# echo "CPN:$cpn PN:$pn TYPE:$type NAME:$name CPROP:$cprop VAL:$val<BR>";
}

// Set a content's source/properties from the PCACHE
function ContentSetProp($pn, $type, $name, $val, $cprop=null) {
  global $SaveProperties, $ContentCfgPropPrefix;
  $prop = "$ContentCfgPropPrefix${type}_$name";
  if($cprop) $prop .= ".$cprop";
  $SaveProperties[] = $prop;
  SetProperty($pn, $prop, $val);
}

// Clear any content page properties for a wikipage array
function ContentClearProperties($pn,&$page,&$new) {
  global $SaveProperties, $PCache, $ContentCfgPropPrefix,
         $ContentEditing, $EnablePost;
  $ContentEditing = true;
  if($EnablePost) ContentClearCache($pn);
  foreach($new as $k=>$v)
    if(substr($k, 0, strlen($ContentCfgPropPrefix))
       == $ContentCfgPropPrefix) {
      unset($PCache[$pn]["=p_$k"]);
      $SaveProperties[] = "$k";
    } 
}

function ContentHandleFullText($fulltext) {
  return str_replace(array('&lt;', '&gt;', '&amp;', '<:vspace>', '\"'), array('<', '>', '&', '', '"'),  $fulltext);
}

// Fill a cp struct with a content's data from the PCACHE
function ContentCPFillData($cpn, $cp) {
  // Do we eventually want to return false on unautorized?
  $cp['data'] = ContentGetProp($cpn, $cp['pagename'], $cp['type'], $cp['name']);
  $defargs = ContentGetProp($cpn, $cp['pagename'], $cp['type'], $cp['name'], 'args');
  $args = ContentParseCnvArgs($defargs, $cp['type']);
  return ContentCPApplyCArgs($cp, $args);
}

// Insert the Content as an attribute in the wiki page text
function ContentSave($pn, $csrc, $type, $name='', $defargs=null) {
  global $ContentTypes, $ContentTypeNames;
  if (!$type) return;
  if ($name == '')  $name = ++$ContentTypeNames[$type];
  $handler = $ContentTypes[$type]['handler'];
  if ($handler)  $csrc = $handler($pn, $type, $name, $csrc);

  ContentSetProp($pn, $type, $name, $csrc);
  ContentSetProp($pn, $type, $name, $defargs, 'args');

  if(isset($_GET['contentclear'])) ContentClearCache($pn);
  return "(:contentset $type $name:)";
}

// Convert a cpath or cp struct to a URL
// ~we no longer seem to need cpath support here
//  withouth it, cpn can be replaced with cp['pagename']
function ContentURL($cppath, $cpn) {
  global $ContentEditing, $ContentCfgPreviewKey;
  if($cppath == '/') return;
  if(is_array($cppath))  $cp = $cppath;
  else                   $cp = ContentParsePath($cppath, $cpn);
  $cpath = ContentMkPath($cp);

  $ext = $cp['tsegs'][count($cp['tsegs'])-1]['ext'];
  $cargs  = "action=content";
  if ($ContentEditing && isset($_POST['preview'])) {
    $cp = ContentCPFillData($cpn, $cp);
    if($ContentCfgPreviewKey) {
      $md5 = md5(ContentMkPath($cp).$cp['data'].$ContentCfgPreviewKey);
      $cargs .= "&content_md5=".urlencode($md5);
    }
    $cargs .= "&content_data=".urlencode($cp['data']);
  }
  $cargs .= "&content=$cpath".($ext ? ".$ext":'');
  $fname = $cp['name'].($ext ? ".$ext":'');
  $url = FmtPageName("\$ScriptUrl", $cp['pagename']);
  $pn = FmtPageName("\$FullName", $cp['pagename']);

  // We always use this format even with clean URLs so that
  // the file (pre-query string) part of the URL ends in
  // a filename and extension.  Some browsers will use this
  // filename when saving the content.
  $url .= "/$fname?n=$pn&$cargs";
  return $url;
}

// Lookup all the output types of a type.
// Returns an array indexed by otypes.
function ContentPathsFrom($itype, $name=null) {
  global $ContentIndex;

  $cpaths[$itype] = "/$itype~$name";
  foreach((array) $ContentIndex['in_out'][$itype] as $otype =>$cnv) {
    $cpaths[$otype] = ContentPath2($itype, $otype);
    if($name) $cpaths[$otype] = $cpaths[$otype]."~$name"; 
  }
  return $cpaths;
}

// Apply converter arguments to cpaths
function ContentApplyCArgs($cpaths, $cargs) {
  foreach((array) $cpaths as $otype => $cpath) {
    $cp = ContentParsePath($cpath);
    $cp = ContentCPApplyCArgs($cp, $cargs);
    $cpaths[$otype] = ContentMkPath($cp);
  }
  return $cpaths;
}

// Apply converter arguments to a cp struct
function ContentCPApplyCArgs($cp, $cargs) {
  foreach($cp['tsegs'] as $k => $tseg) {
    $targs = $tseg['args'];
    if ($targs === null)
      $cp['tsegs'][$k]['args'] = $cargs[$tseg['cnv']];
  }
  return $cp;
}

// Take an args list and output arguments indexed by converters
function ContentParseCnvArgs($args, $itype) {
  foreach(explode(',', $args) as $targs) {
    if($targs === '' || $targs === null) continue;
    $cp = ContentParsePath("/$itype..$targs");
    $tseg = $cp['tsegs'][count($cp['tsegs']) -1];
    $oargs[$tseg['cnv']] = $tseg['args'];
  }
  return $oargs;
}

// Convert a tseg struct to a seg
function ContentMkSeg($tseg, $ptype=null) {
    $type = $tseg['type'];
    $cnv = $tseg['cnv'];
    $args = $tseg['args'];
    if($args !== null)  $args = ".$args";

    if ($ptype && $cnv == $ptype .'2'. $type)  return "/$type$args";
    return "/$cnv$args:$type";
}

// Convert a cp struct to a cpath without {$...}
// (they are needed/used in url cpaths)
function ContentMkPath($cp) {
  $ptype = $cp['type'];
  $cpath = "/$ptype";
  $args = $cp['tsegs'][0]['args'];
  if($args !== null)  $cpath .= ".$args";
  for($i=1; $i< count($cp['tsegs']) ; $i++) {
    $tseg = $cp['tsegs'][$i];
    $cpath .= ContentMkSeg($tseg, $ptype);
    $ptype = $tseg['type'];
  }
  if(isset($cp['name']))  $cpath .= '~'. $cp['name'];
  return $cpath;
}

// Traverse the type section of a cpath and expand it
function ContentExpandPath($tpath) {
  while($otpath != $tpath) {
    $otpath=$tpath;
    # otypes ending with a dot work by virtue of the dot staying in place
    $tpath = preg_replace('|/([^/]+?)/?\.\./?([^/~.]+(\.[^/~.]+)*)|e', 'ContentPath2("$1", "$2")', $tpath);
    $tpath = preg_replace('|//+|', '/', $tpath);
  }
  return $tpath;
}

// Parses a cpath segment and returns a tseg struct
function ContentParseTseg($tspath, $itype=null) {
  global $ContentTypes, $ContentConverters, $ContentIndex;
  preg_match('|^([^:]*)(?::(.*))?$|', $tspath, $m);
  list($x, $cnvseg, $type) = $m;
  preg_match('/^(.*?)(\.(.*))?$/', $type, $m);
  list($x, $type, $dot, $targs) = $m;
  if($targs == null) $targs=''; if($dot == null)  $targs=null;
  preg_match('/^(.*?)(\.(.*))?$/', $cnvseg, $m);
  list($x, $cnv, $dot, $cargs) = $m;
  if($cargs == null) $cargs=''; if($dot == null)  $cargs= $targs;
  if (!$type) {
    if($itype) {
      $otype = $ContentConverters[$cnv]['outtype'];
      if(isset($ContentTypes[$cnv]) || !$otype) {
        $type = $cnv;
        $cnv = $itype .'2'. $type;
        if($ContentConverters[$cnv]['outtype'] !== $type)  $cnv=null;
      } else $type = $otype;
    } else {
      if(isset($ContentTypes[$cnv])) $type = $cnv;
      $cnv = null;
    }
  }
  return array('type'=>$type, 'cnv'=>$cnv, 'args'=>$cargs,
               'mime' => $ContentTypes[$type]['mime'],
               'ext' =>  $ContentTypes[$type]['ext'],
               'intype' => $itype,
               'inext' => $ContentTypes[$itype]['ext']
        );
}

// Convert a cpath to a cp struct
// Internal short cpaths we shave to deal with:
//    /abcm         (no ~<name>)
//    /abcm/ps~1
//    {PN$/abcm}
function ContentParsePath($cpath, $pn=null) {
  global $ContentPageNamePat;
  preg_match("|^\{?(?:($ContentPageNamePat)?".'\$)?(/.*?)(?:~([^~.}]*)(?:\.([^.}]*))?)?}?$|', $cpath, $m);
  list($x, $cppn, $tpath, $name, $ext) = $m;
  if($cppn) $pn = $cppn;
  $tpath = ContentExpandPath($tpath);
  $tspaths = explode('/', $tpath);
  foreach($tspaths as $tspath) {
    if($tspath) {
      $tseg = ContentParseTseg($tspath, $prevtype);
      $prevtype = $tseg['type'];
      $tsegs[] = $tseg;
    }
  }
  return array('name'=>$name, 'type'=>$tsegs[0]['type'], 'tsegs'=>$tsegs,
               'pagename' => $pn);
}


// Get the content output for a cp struct
// cp['data'] must be set and no authorization is done here!
/* Walk the cpath recursively for each path segment
   processing the data with the appropriate fitler
   along the way.  For each path segment, this function 
   gets called first to check for any cached version of 
   the current path segment output, if nothing is 
   cached it calls itself again for the same path
   segment so that it can process and store the results
   in the cache for next time. */
function ContentGetCP($cp, $cache=true) {
  global $ContentCfgPropPrefix, $ContentConverters;

  if(!isset($cp['data']))  return null;
  $out = ContentCacheGet($cp);
  if($out !== null) return $out;

  if($cache)  return ContentCacheSave($cp, ContentGetCP($cp, false));

  $tsegs= $cp['tsegs']; $name= $cp['name']; $c= count($tsegs); $l= $c - 1;

  if($c == 0) return null;
    // ~ContentError($cp, HTTP_INTERNALSERVER); not for wiki pages!
  if($c == 1) return $cp['data'];

  $cnv = $tsegs[$l]['cnv'];
  if (!$cnv) return null;
    // ~ContentError($cp, HTTP_INTERNALSERVER); not for wiki pages!

  $fnc     = $ContentConverters[$cnv]['fnc'];
  $intype  = $ContentConverters[$cnv]['intype'];
  $outtype = $ContentConverters[$cnv]['outtype'];
  $ptype = $tsegs[$l-1]['type'];
  $ctype = $tsegs[$l]['type'];
  if (!$fnc  || $intype  != $ptype || $outtype != $ctype) {
    if (!$fnc) echo "CONTENT_CONVERTER ($cnv) function is blank\n";
    if ($intype != $ptype)
      echo "CONTENT_CONVERTER ($cnv) input type does not match ($intype != $ptype)\n";
    if ($outtype != $ctype)
      echo "CONTENT_CONVERTER ($cnv) output type does not match ($outtype != $ctype)\n";
    sleep(10); return null; // ~sleep should be only for writting?
    // ~ContentError($cp, HTTP_INTERNALSERVER); not for wiki pages!
  }
  $args = $tsegs[$l]['args'];

  $cpi = $cp;
  array_pop($cpi['tsegs']);
  $out = ContentGetCP($cpi);
  if(!$out) return false; // do not cache any further!

  if(is_array($out)) {
    foreach($out as $k => $cpath) {
      $cpo = ContentParsePath($cpath, $cp['pagename']);
      $cpo['tsegs'][$l] = $tsegs[$l];
      $out[$k] = ContentMkPath($cpo);
    }
    return $out;
  }

  $out = $fnc($cp, $cnv, $intype, $outtype, $args, $out);

  if(is_array($out))
    foreach($out as $k => $v) {
      $cpo = $cp;
      $cpo['tsegs'][$l]['args'] = $k;
      $out[$k] = ContentMkPath($cpo);
    }
  return $out;
}

// Called by pmwiki when the logfile for a page is requested
function ContentActionLog($pn, $auth="read") {
  $log = ContentGetLog($pn, $auth);
  if($log === false) {
    echo "You are not authorized to view this log, sorry. <BR>";
    return;
  }
  echo $log;
}

// Called by pmwiki during saves after page has been written
function ContentAfterSave($pn,&$page,&$new) {
  global $FmtV;
  if (@$_POST['preview']) {
    $log = ContentGetLog($pn, 'edit');
    ContentClearCache($pn);
    if(!$log) return;
    $FmtV['$PreviewText'] = $FmtV['$PreviewText'] . "\n<hr/>\n$log";
  }
}

// Called by pmwiki when the content for a cpath is requested
function ContentAction($pn, $auth="read") {
  global $ContentCfgPreviewKey, $ContentCfgPropPrefix,
         $ContentCacheEnable, $ContentTypes;
  if(isset($_GET['contentclear'])) ContentClearCache($pn);
  $cpath = stripmagic($_GET["content"]);
  $cp = ContentParsePath($cpath, $pn);

  if(isset($_REQUEST["content_data"])) {
    if($ContentTypes[$cp['type']]['directive'] === false)
      ContentError($cp, HTTP_FORBIDDEN);
    if(!CondAuth($pn, 'edit'))
      ContentError($cp, HTTP_UNAUTHORIZED);
    $ContentCacheEnable = false;
    $cp['data'] = stripmagic($_REQUEST["content_data"]);
    if($ContentCfgPreviewKey) {
      $md5 = md5(ContentMkPath($cp).$cp['data'].$ContentCfgPreviewKey);
      if ($_GET['content_md5'] != $md5) 
        ContentError($cp, HTTP_UNAUTHORIZED);
    }
  } else {
    $page = RetrieveAuthPage($cp['pagename'], 'read', true,
        READPAGE_CURRENT);
    $prop = $ContentCfgPropPrefix.$cp['type']."_".$cp['name'];
    if(!isset($page[$prop])) ContentError($cp, HTTP_NOTFOUND);
    $cp['data'] = $page[$prop];
    $args = ContentParseCnvArgs($page[$prop.'.args'], $cp['type']);
    $cp = ContentCPApplyCArgs($cp, $args);
  }

  $out = ContentGetCP($cp);
  if(!$out) ContentError($cp, HTTP_NOCONTENT);

  $c = count($cp['tsegs']);
  $mime = $cp['tsegs'][$c-1]['mime'];
  if ($mime)  header("Content-type: ". $mime);
  echo "$out";  # The main output, not a debug statement :)
}

function ContentCachePageDir($pn) {
  global $ContentCfgCacheDir, $ContentCache, $ContentCacheEnable;
  if(!$ContentCacheEnable) return null;
  if (@$_POST['preview']) {
    $dir = $ContentCache[$pn]['dir'];
    if(!$dir) {
      $dir = ContentTmpDir();
      $ContentCache[$pn]['dir']= $dir;
    }
  } else $dir = "$ContentCfgCacheDir/cache/$pn";
  return $dir;
}

// API: Output text to the cp's page logfile
function ContentLog($cp, $text) {
  global $ContentCacheEnable;
  if(!$ContentCacheEnable) return $data;
  $dir = ContentCachePageDir($cp['pagename']);
  $file = "$dir/Content.log";
  file_put_contents($file, $text, FILE_APPEND);
}

function ContentGetLog($pn, $auth="read") {
  global $ContentCfgLogViewer, $ContentCacheEnable;
  if(!$ContentCacheEnable) return null;
  $dir = ContentCachePageDir($pn);
  $file = "$dir/Content.log";
  if(CondAuth($pn, $ContentCfgLogViewer))  return false;
  $log = @file_get_contents($file);
  if(!$log) return;
  $log = implode("<BR/>\n", explode("\n", $log));
  return "LOG FILE for $pn<p><code>$log</code></p>";
}

// Convert a cp struct to a cached filename
function ContentCacheFileName($cp) {
  global $ContentCacheEnable;
  if(!$ContentCacheEnable) return null;
  $cpath = ContentMkPath($cp);
  $cpath = preg_replace('|/|', '~',  substr($cpath,1));
  $dir = ContentCachePageDir($cp['pagename']);
  $ext = $cp['tsegs'][count($cp['tsegs'])-1]['ext'];
  $file = "$dir/$cpath".($ext ? ".$ext":'');
  return $file;
}

// Clear the cache for a wikipage
function ContentClearCache($pn) {
  global $ContentCacheEnable;
  if(!$ContentCacheEnable) return;
  ContentRmTree(ContentCachePageDir($pn));
}

// API: Import a file into the cache system
function ContentCachePutFile($cp, $file) {
  global $ContentCacheEnable;
  if(!$ContentCacheEnable) { 
    @unlink($file);
    return;
  }
  if(!file_exists($file))  return;
  $cfile = ContentCacheFileName($cp);
  ContentLog($cp, "=** IMPORTED: $file\n    TO CACHE: $cfile\n\n");
  @rename($file, $cfile);
}

// Save data in the cache system
//  if data is null or false, do not save!
function ContentCacheSave($cp, $data) {
  global $ContentCacheEnable;
  if(!$ContentCacheEnable || $data === null || $data === false) return $data;
  $file = ContentCacheFileName($cp);
  $filea = "$file.array";
  if(is_array($data)) {
    @unlink($file);
    if(file_exists($filea))  return $data;
  } else {
    @unlink($filea);
    if(file_exists($file))  return $data;
  }

  $dir = dirname($file);
  mkdirp($dir);
  if(is_array($data)) {
    $file = $filea;
    $sdata = serialize($data);
    file_put_contents($file, $sdata);
  } else {
    file_put_contents($file, $data);
  }
  ContentLog($cp, "=** CACHED: $file\n\n");
  return $data;
}

// Get data from the cache
//  null means data was not in cache or cache is disabled
function ContentCacheGet($cp) {
  global $ContentCacheEnable;
  if(!$ContentCacheEnable) return null;
  $file = ContentCacheFileName($cp);
  if(file_exists("$file.array")) {
    return unserialize(file_get_contents("$file.array"));
  }
  if(! file_exists($file)) return null;
  return file_get_contents($file);
}


// API: Escape an array of arguments safely for shell use
function ContentEscapeShellArgs($args) {
  foreach($args as $k=>$v) $out[$k]= escapeshellarg($v);
  return (array)$out;
}

// Useless absraction, deprecated, but still used by music.php
function ContentDir($cp) { return ContentTmpDir(); }

// This runs a system command feeding it input and output files
function ContentFSConverter($cp, $cnv, $intype, $outtype, $args, $data) {
  global $ContentConverters;

  $c = count($cp['tsegs']) - 1;

  ContentLog($cp, "FS Converter: ==== $cnv ====\n");
  $fnc = $ContentConverters[$cnv]['cmd'];
  if($fnc) {
    $cmd = $fnc($cp, $cnv, $intype, $outtype, $args);
  } else {
    $cmd = $ContentConverters[$cnv]['cmdfmt'];
    $fnc = $ContentConverters[$cnv]['argfnc'];
    if($fnc) {
      ContentLog($cp, "REPLACING ARGS($args): $cmd\n");
      if($args) { 
        $argv = $fnc($cp, $cnv, $intype, $outtype, explode('.', $args));
        $args = implode(" ", ContentEscapeShellArgs($argv));
      }
      $cmd = str_replace('${a}', $args, $cmd);
    }
  }

  $dir = ContentTmpDir();
  $iext = $cp['tsegs'][$c]['inext'];
  $oext = $cp['tsegs'][$c]['ext'];
  $tmpi = "$dir/I_$cnv".($iext ? ".$iext" : '');
  $tmpo = "$dir/O_$cnv".($oext ? ".$oext" : '');
  $cmd = str_replace('${i}', $tmpi, $cmd);
  $cmd = str_replace('${o}', $tmpo, $cmd);

  file_put_contents($tmpi, $data);

  ContentLog($cp, "EXECUTING: $cmd\n");
  exec($cmd, $err);
  if($err) ContentLog($cp, "ERROR:\n".implode("\n", $err)."\n");

  $out = @file_get_contents($tmpo);
  ContentCachePutFile($cp, $tmpo);
  if (!$ContentConverters[$cnv]['keep']) ContentRmTree($dir);
  if(!$out) return ''; // Cache empty results too
  return $out;
}

// Create a temporary file (atomic on unix)
function ContentTmpname($dir, $pre='', $post='') {
  $tmpc = tempnam($dir, $pre);
  $tmpn = $tmpc . $post;
  if ($tmpc != $tmpn) {
    $win = DIRECTORY_SEPARATOR == '\\' ? true : false;
    $link = $win ? 'rename' : 'link';
    while (! @$link($tmpc, $tmpn)) {
      $tmpn = $tmpc . $i++ . $post;
      if ($i > 10000) return false;
    }
    if(! $win) unlink($tmpc);
  }
  return $tmpn;
}

function ContentTmpDir($dir=null, $pre='', $post='') {
  global $ContentCfgCacheDir;
  if($dir ===null)  $dir = "$ContentCfgCacheDir/tmp";
  mkdirp($dir);
  $tmp = "$dir/$pre".(++$i).$post;
  while (! @mkdir($tmp, 0755)) {
    if ($i > 10000) return false;
    $tmp = "$dir/$pre".(++$i).$post;
  }
  return $tmp;
}

// rm -rf
function ContentRmTree($dir) {
  $dir = escapeshellarg($dir);
  exec("rm -rf $dir");
}

if ( !function_exists('file_put_contents') && !defined('FILE_APPEND') ) {
  define('FILE_APPEND', 1);
  function file_put_contents($n, $d, $flag = false) {
    $mode = ($flag == FILE_APPEND || 
            strtoupper($flag) == 'FILE_APPEND') ? 'a' : 'w';
    $f = @fopen($n, $mode);
    if ($f === false) return false;
    if (is_array($d)) $d = implode($d);
    $bytes = fwrite($f, $d);
    fclose($f);
    return $bytes;
  }
}

define('HTTP_FORBIDDEN', '403 Request Forbidden');
define('HTTP_NOTFOUND', '404 Content Not Found');
define('HTTP_NOCONTENT', '204 No Content');
define('HTTP_UNAUTHORIZED', '401 Unauthorized');
define('HTTP_INTERNALSERVER', '500 Internal Server Error');

function ContentError($cp, $err, $exit=true) {
  header("HTTP/1.0 $err");
  $cpath = '{'.$cp['pagename'].'$'.ContentMkPath($cp).'}';
  echo "PmWiki Content Recipe For: $cpath</br>";
  echo "</br>\n$err";
  if($exit) exit();
};
